package com.googlecode.afdbaccess.web.common.servletfilter.sessionlesscookieauthentication;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.jboss.security.Base64Utils;

/**
 * Encrypts a byte array, compresses it and base 64 encodes it to a String. Can also decrypt the resulting string.
 * If you get an exception about key length being invalid, you may have to install
 * "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files" into your runtime.
 */
class ByteArrayCrypter {
    //private final static Logger log = Logger.getLogger(ByteArrayCrypter.class);
    
    private SecretKeySpec key;

    ByteArrayCrypter(String keyString) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] salt = {-93, 12, 69, 18, -11, 0, -127, 119};
        
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        KeySpec spec = new PBEKeySpec(keyString.toCharArray(), salt, 1024, 256);
        key = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
    }

    String encrypt(byte[] message) throws InvalidKeyException, IOException, IllegalBlockSizeException, BadPaddingException, InvalidParameterSpecException, NoSuchAlgorithmException, NoSuchPaddingException {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] iv = cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
        byte[] encryptedBytes = cipher.doFinal(message);
        
        //log.info("iv length: " + iv.length + " encryptedBytes length: " + encryptedBytes.length);

        byte[] result = concat(iv, encryptedBytes);
        
        //log.info("iv length: " + iv.length);
        //log.info("size: " + result.length + "/" + Base64Utils.tob64(result).length());
        //log.info("Compressed size: " + compress(result).length);
        
//        byte[] compressed = compress(result);
//        StringBuilder byteStringBuilder = new StringBuilder();
//        for (int counter = 0; counter < 20; counter++) {
//            byteStringBuilder.append(compressed[counter] + ", ");
//        }
//        log.info("firstBytes: " + byteStringBuilder);
        
        return Base64Utils.tob64(compress(result));
    }

    private static byte[] concat(byte[] first, byte[] second) {
        byte[] result = Arrays.copyOf(first, first.length + second.length);
        System.arraycopy(second, 0, result, first.length, second.length);
        return result;
    }
    
    byte[] decrypt(String encrypted) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, IOException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
        byte[] mergedBytes = decompress(Base64Utils.fromb64(encrypted));
        byte[] iv = Arrays.copyOf(mergedBytes, 16);
        byte[] encryptedBytes = Arrays.copyOfRange(mergedBytes, 16, mergedBytes.length);

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
        return cipher.doFinal(encryptedBytes);
    }

    private byte[] compress(byte[] bytesToCompress) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
        GZIPOutputStream out = new GZIPOutputStream(baos);
        try {
            out.write(bytesToCompress);
        } finally {
            out.close();
        }

        return baos.toByteArray();
    }
    
    private byte[] decompress(byte[] bytesToDecompress) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytesToDecompress);
        GZIPInputStream in = new GZIPInputStream(bais);
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
            try {
                byte[] buff = new byte[1024];
                int count = 0;
                while(count >= 0) {
                    count = in.read(buff);
                    
                    if (count > 0) {
                        baos.write(buff, 0, count);
                    }
                }
            } finally {
                baos.close();
            }
            return baos.toByteArray();
        } finally {
            in.close();
        }
    }
}
